// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/app_list/app_list_model.h"

#include <string>
#include <utility>

#include "ui/app_list/app_list_folder_item.h"
#include "ui/app_list/app_list_item.h"
#include "ui/app_list/app_list_model_observer.h"
#include "ui/app_list/search_box_model.h"

namespace app_list {

AppListModel::AppListModel()
    : top_level_item_list_(new AppListItemList)
    , search_box_(new SearchBoxModel)
    , results_(new SearchResults)
    , status_(STATUS_NORMAL)
    , state_(INVALID_STATE)
    , folders_enabled_(false)
    , custom_launcher_page_enabled_(true)
    , search_engine_is_google_(false)
{
    top_level_item_list_->AddObserver(this);
}

AppListModel::~AppListModel() { top_level_item_list_->RemoveObserver(this); }

void AppListModel::AddObserver(AppListModelObserver* observer)
{
    observers_.AddObserver(observer);
}

void AppListModel::RemoveObserver(AppListModelObserver* observer)
{
    observers_.RemoveObserver(observer);
}

void AppListModel::SetStatus(Status status)
{
    if (status_ == status)
        return;

    status_ = status;
    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListModelStatusChanged());
}

void AppListModel::SetState(State state)
{
    if (state_ == state)
        return;

    State old_state = state_;

    state_ = state;

    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListModelStateChanged(old_state, state_));
}

AppListItem* AppListModel::FindItem(const std::string& id)
{
    AppListItem* item = top_level_item_list_->FindItem(id);
    if (item)
        return item;
    for (size_t i = 0; i < top_level_item_list_->item_count(); ++i) {
        AppListItem* child_item = top_level_item_list_->item_at(i)->FindChildItem(id);
        if (child_item)
            return child_item;
    }
    return NULL;
}

AppListFolderItem* AppListModel::FindFolderItem(const std::string& id)
{
    AppListItem* item = top_level_item_list_->FindItem(id);
    if (item && item->GetItemType() == AppListFolderItem::kItemType)
        return static_cast<AppListFolderItem*>(item);
    DCHECK(!item);
    return NULL;
}

AppListItem* AppListModel::AddItem(scoped_ptr<AppListItem> item)
{
    DCHECK(!item->IsInFolder());
    DCHECK(!top_level_item_list()->FindItem(item->id()));
    return AddItemToItemListAndNotify(std::move(item));
}

AppListItem* AppListModel::AddItemToFolder(scoped_ptr<AppListItem> item,
    const std::string& folder_id)
{
    if (folder_id.empty())
        return AddItem(std::move(item));
    DVLOG(2) << "AddItemToFolder: " << item->id() << ": " << folder_id;
    CHECK_NE(folder_id, item->folder_id());
    DCHECK_NE(AppListFolderItem::kItemType, item->GetItemType());
    AppListFolderItem* dest_folder = FindOrCreateFolderItem(folder_id);
    if (!dest_folder)
        return NULL;
    DCHECK(!dest_folder->item_list()->FindItem(item->id()))
        << "Already in folder: " << dest_folder->id();
    return AddItemToFolderItemAndNotify(dest_folder, std::move(item));
}

const std::string AppListModel::MergeItems(const std::string& target_item_id,
    const std::string& source_item_id)
{
    if (!folders_enabled()) {
        LOG(ERROR) << "MergeItems called with folders disabled.";
        return "";
    }
    DVLOG(2) << "MergeItems: " << source_item_id << " -> " << target_item_id;

    if (target_item_id == source_item_id) {
        LOG(WARNING) << "MergeItems tried to drop item onto itself ("
                     << source_item_id << " -> " << target_item_id << ").";
        return "";
    }

    // Find the target item.
    AppListItem* target_item = top_level_item_list_->FindItem(target_item_id);
    if (!target_item) {
        LOG(ERROR) << "MergeItems: Target no longer exists.";
        return "";
    }

    AppListItem* source_item = FindItem(source_item_id);
    if (!source_item) {
        LOG(ERROR) << "MergeItems: Source no longer exists.";
        return "";
    }

    // If the target item is a folder, just add the source item to it.
    if (target_item->GetItemType() == AppListFolderItem::kItemType) {
        AppListFolderItem* target_folder = static_cast<AppListFolderItem*>(target_item);
        if (target_folder->folder_type() == AppListFolderItem::FOLDER_TYPE_OEM) {
            LOG(WARNING) << "MergeItems called with OEM folder as target";
            return "";
        }
        scoped_ptr<AppListItem> source_item_ptr = RemoveItem(source_item);
        source_item_ptr->set_position(
            target_folder->item_list()->CreatePositionBefore(
                syncer::StringOrdinal()));
        AddItemToFolderItemAndNotify(target_folder, std::move(source_item_ptr));
        return target_folder->id();
    }

    // Otherwise remove the source item and target item from their current
    // location, they will become owned by the new folder.
    scoped_ptr<AppListItem> source_item_ptr = RemoveItem(source_item);
    CHECK(source_item_ptr);
    // Note: This would fail if |target_item_id == source_item_id|, except we
    // checked that they are distinct at the top of this method.
    scoped_ptr<AppListItem> target_item_ptr = top_level_item_list_->RemoveItem(target_item_id);
    CHECK(target_item_ptr);

    // Create a new folder in the same location as the target item.
    std::string new_folder_id = AppListFolderItem::GenerateId();
    DVLOG(2) << "Creating folder for merge: " << new_folder_id;
    scoped_ptr<AppListItem> new_folder_ptr(new AppListFolderItem(
        new_folder_id, AppListFolderItem::FOLDER_TYPE_NORMAL));
    new_folder_ptr->set_position(target_item_ptr->position());
    AppListFolderItem* new_folder = static_cast<AppListFolderItem*>(
        AddItemToItemListAndNotify(std::move(new_folder_ptr)));

    // Add the items to the new folder.
    target_item_ptr->set_position(
        new_folder->item_list()->CreatePositionBefore(
            syncer::StringOrdinal()));
    AddItemToFolderItemAndNotify(new_folder, std::move(target_item_ptr));
    source_item_ptr->set_position(
        new_folder->item_list()->CreatePositionBefore(
            syncer::StringOrdinal()));
    AddItemToFolderItemAndNotify(new_folder, std::move(source_item_ptr));

    return new_folder->id();
}

void AppListModel::MoveItemToFolder(AppListItem* item,
    const std::string& folder_id)
{
    DVLOG(2) << "MoveItemToFolder: " << folder_id
             << " <- " << item->ToDebugString();
    if (item->folder_id() == folder_id)
        return;
    AppListFolderItem* dest_folder = FindOrCreateFolderItem(folder_id);
    scoped_ptr<AppListItem> item_ptr = RemoveItem(item);
    if (dest_folder) {
        CHECK(!item->IsInFolder());
        AddItemToFolderItemAndNotify(dest_folder, std::move(item_ptr));
    } else {
        AddItemToItemListAndNotifyUpdate(std::move(item_ptr));
    }
}

bool AppListModel::MoveItemToFolderAt(AppListItem* item,
    const std::string& folder_id,
    syncer::StringOrdinal position)
{
    DVLOG(2) << "MoveItemToFolderAt: " << folder_id
             << "[" << position.ToDebugString() << "]"
             << " <- " << item->ToDebugString();
    if (item->folder_id() == folder_id)
        return false;
    AppListFolderItem* src_folder = FindOrCreateFolderItem(item->folder_id());
    if (src_folder && src_folder->folder_type() == AppListFolderItem::FOLDER_TYPE_OEM) {
        LOG(WARNING) << "MoveItemToFolderAt called with OEM folder as source";
        return false;
    }
    AppListFolderItem* dest_folder = FindOrCreateFolderItem(folder_id);
    scoped_ptr<AppListItem> item_ptr = RemoveItem(item);
    if (dest_folder) {
        item_ptr->set_position(
            dest_folder->item_list()->CreatePositionBefore(position));
        AddItemToFolderItemAndNotify(dest_folder, std::move(item_ptr));
    } else {
        item_ptr->set_position(
            top_level_item_list_->CreatePositionBefore(position));
        AddItemToItemListAndNotifyUpdate(std::move(item_ptr));
    }
    return true;
}

void AppListModel::SetItemPosition(AppListItem* item,
    const syncer::StringOrdinal& new_position)
{
    if (!item->IsInFolder()) {
        top_level_item_list_->SetItemPosition(item, new_position);
        // Note: this will trigger OnListItemMoved which will signal observers.
        // (This is done this way because some View code still moves items within
        // the item list directly).
        return;
    }
    AppListFolderItem* folder = FindFolderItem(item->folder_id());
    DCHECK(folder);
    folder->item_list()->SetItemPosition(item, new_position);
    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListItemUpdated(item));
}

void AppListModel::SetItemName(AppListItem* item, const std::string& name)
{
    item->SetName(name);
    DVLOG(2) << "AppListModel::SetItemName: " << item->ToDebugString();
    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListItemUpdated(item));
}

void AppListModel::SetItemNameAndShortName(AppListItem* item,
    const std::string& name,
    const std::string& short_name)
{
    item->SetNameAndShortName(name, short_name);
    DVLOG(2) << "AppListModel::SetItemNameAndShortName: "
             << item->ToDebugString();
    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListItemUpdated(item));
}

void AppListModel::DeleteItem(const std::string& id)
{
    AppListItem* item = FindItem(id);
    if (!item)
        return;
    if (!item->IsInFolder()) {
        DCHECK_EQ(0u, item->ChildItemCount())
            << "Invalid call to DeleteItem for item with children: " << id;
        FOR_EACH_OBSERVER(AppListModelObserver,
            observers_,
            OnAppListItemWillBeDeleted(item));
        top_level_item_list_->DeleteItem(id);
        FOR_EACH_OBSERVER(AppListModelObserver, observers_, OnAppListItemDeleted());
        return;
    }
    AppListFolderItem* folder = FindFolderItem(item->folder_id());
    DCHECK(folder) << "Folder not found for item: " << item->ToDebugString();
    scoped_ptr<AppListItem> child_item = RemoveItemFromFolder(folder, item);
    DCHECK_EQ(item, child_item.get());
    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListItemWillBeDeleted(item));
    child_item.reset(); // Deletes item.
    FOR_EACH_OBSERVER(AppListModelObserver, observers_, OnAppListItemDeleted());
}

void AppListModel::DeleteUninstalledItem(const std::string& id)
{
    AppListItem* item = FindItem(id);
    if (!item)
        return;
    const std::string folder_id = item->folder_id();
    DeleteItem(id);

    // crbug.com/368111: Upon uninstall of 2nd-to-last folder item, reparent last
    // item to top; this will remove the folder.
    AppListFolderItem* folder = FindFolderItem(folder_id);
    if (folder && folder->ChildItemCount() == 1u) {
        AppListItem* last_item = folder->item_list()->item_at(0);
        MoveItemToFolderAt(last_item, "", folder->position());
    }
}

void AppListModel::NotifyExtensionPreferenceChanged()
{
    for (size_t i = 0; i < top_level_item_list_->item_count(); ++i)
        top_level_item_list_->item_at(i)->OnExtensionPreferenceChanged();
}

void AppListModel::SetFoldersEnabled(bool folders_enabled)
{
    folders_enabled_ = folders_enabled;
    if (folders_enabled)
        return;
    // Remove child items from folders.
    std::vector<std::string> folder_ids;
    for (size_t i = 0; i < top_level_item_list_->item_count(); ++i) {
        AppListItem* item = top_level_item_list_->item_at(i);
        if (item->GetItemType() != AppListFolderItem::kItemType)
            continue;
        AppListFolderItem* folder = static_cast<AppListFolderItem*>(item);
        if (folder->folder_type() == AppListFolderItem::FOLDER_TYPE_OEM)
            continue; // Do not remove OEM folders.
        while (folder->item_list()->item_count()) {
            scoped_ptr<AppListItem> child = folder->item_list()->RemoveItemAt(0);
            child->set_folder_id("");
            AddItemToItemListAndNotifyUpdate(std::move(child));
        }
        folder_ids.push_back(folder->id());
    }
    // Delete folders.
    for (size_t i = 0; i < folder_ids.size(); ++i)
        DeleteItem(folder_ids[i]);
}

void AppListModel::SetCustomLauncherPageEnabled(bool enabled)
{
    custom_launcher_page_enabled_ = enabled;
    FOR_EACH_OBSERVER(AppListModelObserver, observers_,
        OnCustomLauncherPageEnabledStateChanged(enabled));
}

std::vector<SearchResult*> AppListModel::FilterSearchResultsByDisplayType(
    SearchResults* results,
    SearchResult::DisplayType display_type,
    size_t max_results)
{
    std::vector<SearchResult*> matches;
    for (size_t i = 0; i < results->item_count(); ++i) {
        SearchResult* item = results->GetItemAt(i);
        if (item->display_type() == display_type) {
            matches.push_back(item);
            if (matches.size() == max_results)
                break;
        }
    }
    return matches;
}

void AppListModel::PushCustomLauncherPageSubpage()
{
    custom_launcher_page_subpage_depth_++;
}

bool AppListModel::PopCustomLauncherPageSubpage()
{
    if (custom_launcher_page_subpage_depth_ == 0)
        return false;

    --custom_launcher_page_subpage_depth_;
    return true;
}

void AppListModel::ClearCustomLauncherPageSubpages()
{
    custom_launcher_page_subpage_depth_ = 0;
}

void AppListModel::SetSearchEngineIsGoogle(bool is_google)
{
    search_engine_is_google_ = is_google;
    FOR_EACH_OBSERVER(AppListModelObserver, observers_,
        OnSearchEngineIsGoogleChanged(is_google));
}

// Private methods

void AppListModel::OnListItemMoved(size_t from_index,
    size_t to_index,
    AppListItem* item)
{
    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListItemUpdated(item));
}

AppListFolderItem* AppListModel::FindOrCreateFolderItem(
    const std::string& folder_id)
{
    if (folder_id.empty())
        return NULL;

    AppListFolderItem* dest_folder = FindFolderItem(folder_id);
    if (dest_folder)
        return dest_folder;

    if (!folders_enabled()) {
        LOG(ERROR) << "Attempt to create folder item when disabled: " << folder_id;
        return NULL;
    }

    DVLOG(2) << "Creating new folder: " << folder_id;
    scoped_ptr<AppListFolderItem> new_folder(
        new AppListFolderItem(folder_id, AppListFolderItem::FOLDER_TYPE_NORMAL));
    new_folder->set_position(
        top_level_item_list_->CreatePositionBefore(syncer::StringOrdinal()));
    AppListItem* new_folder_item = AddItemToItemListAndNotify(std::move(new_folder));
    return static_cast<AppListFolderItem*>(new_folder_item);
}

AppListItem* AppListModel::AddItemToItemListAndNotify(
    scoped_ptr<AppListItem> item_ptr)
{
    DCHECK(!item_ptr->IsInFolder());
    AppListItem* item = top_level_item_list_->AddItem(std::move(item_ptr));
    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListItemAdded(item));
    return item;
}

AppListItem* AppListModel::AddItemToItemListAndNotifyUpdate(
    scoped_ptr<AppListItem> item_ptr)
{
    DCHECK(!item_ptr->IsInFolder());
    AppListItem* item = top_level_item_list_->AddItem(std::move(item_ptr));
    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListItemUpdated(item));
    return item;
}

AppListItem* AppListModel::AddItemToFolderItemAndNotify(
    AppListFolderItem* folder,
    scoped_ptr<AppListItem> item_ptr)
{
    CHECK_NE(folder->id(), item_ptr->folder_id());
    AppListItem* item = folder->item_list()->AddItem(std::move(item_ptr));
    item->set_folder_id(folder->id());
    FOR_EACH_OBSERVER(AppListModelObserver,
        observers_,
        OnAppListItemUpdated(item));
    return item;
}

scoped_ptr<AppListItem> AppListModel::RemoveItem(AppListItem* item)
{
    if (!item->IsInFolder())
        return top_level_item_list_->RemoveItem(item->id());

    AppListFolderItem* folder = FindFolderItem(item->folder_id());
    return RemoveItemFromFolder(folder, item);
}

scoped_ptr<AppListItem> AppListModel::RemoveItemFromFolder(
    AppListFolderItem* folder,
    AppListItem* item)
{
    std::string folder_id = folder->id();
    CHECK_EQ(item->folder_id(), folder_id);
    scoped_ptr<AppListItem> result = folder->item_list()->RemoveItem(item->id());
    result->set_folder_id("");
    if (folder->item_list()->item_count() == 0) {
        DVLOG(2) << "Deleting empty folder: " << folder->ToDebugString();
        DeleteItem(folder_id);
    }
    return result;
}

} // namespace app_list
